LambdalithとSingle purpose Lambdaは1つのAPI Gatewayで共存できる
はじめに
最近、Monolith Lambda(以降 Lambdalith)な構成でサーバーレスアプリケーションを実装する事例が増えてきていると思います。
サーバーレスアプリケーションを作る際に、最初はLambdalithで構成し、必要になった場合に Single purpose Lambda と共存させれば良さそう、という意見が見られるようになりました。
今回は実際に Lambdalith と Single purpose Lambda が1つの API Gateway の中で共存できるのか、CDKを用いて実装し試してみました。
Single purpose LambdaとLambdalithの違い
Single purpose Lambda とは
APIのパス・メソッドとLambda が1対1で紐づく構成です。
数年前までこちらの構成が多かったと思います。
特定のパスのみアクセスが多い場合はメモリを増強して対応したり、特定のLambdaのみIAMポリシーでセキュアにしたり、といった個別Lambdaの対応ができる点がメリットです。
Lambdalith とは
複数パスのリクエストを1つのLambdaで捌く構成です。
Lambdaの中ではExpress.jsのようなフレームワークを利用してルーティングをします。
パスごとの細かい設定は出来なくなりますが、開発効率, コールドスタートの頻度軽減, コンテナ環境への移行のしやすさなどがメリットになります。
こちらのブログが詳しいので、ご参照ください。
CDKで実装する際にいろんなこと(リソース数上限, スタック分割, APIパスごとのリソース追加など)を考えなくて良くなるので、個人的にはLambdalithでの構成が好きです。
LambdalithとSingle purpose Lambdaの共存を試してみる
やりたいこと
やりたいことはシンプルです。
Lambdalithの構成に、Single purpose Lambdaを付け足すことができるのか検証していきます。
最初 Lambdalithで構成していたプロジェクトに、何らかの理由でSingle purpose Lambdaを使いたくなった時を想定しています。
CDKの実装
今回の実装はこちらのリポジトリにあります。
import {
aws_apigateway,
aws_lambda,
aws_lambda_nodejs,
aws_logs,
RemovalPolicy,
Stack,
StackProps,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class LambdalithAndSinglePurposeLambdaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const monolithLambda = new aws_lambda_nodejs.NodejsFunction(
this,
'MonolithLambda',
{
architecture: aws_lambda.Architecture.ARM_64,
entry: 'src/lambdalith/entry-point.ts',
runtime: aws_lambda.Runtime.NODEJS_20_X,
bundling: {
forceDockerBundling: false,
},
},
);
const singlePurposeLambda = new aws_lambda_nodejs.NodejsFunction(
this,
'SinglePurposeLambda',
{
architecture: aws_lambda.Architecture.ARM_64,
entry: 'src/single-purpose-lambda/dog.ts',
runtime: aws_lambda.Runtime.NODEJS_20_X,
bundling: {
forceDockerBundling: false,
},
},
);
/**
* REST API を作成
*/
const logGroup = new aws_logs.LogGroup(
this,
'MonolithSinglePurposeTogetherAccessLogs',
);
const restApi = new aws_apigateway.RestApi(
this,
'MonolithSinglePurposeTogether',
{
defaultIntegration: new aws_apigateway.LambdaIntegration(
monolithLambda,
), // Lambdalithの設定
defaultCorsPreflightOptions: {
allowOrigins: aws_apigateway.Cors.ALL_ORIGINS,
allowMethods: aws_apigateway.Cors.ALL_METHODS,
allowHeaders: aws_apigateway.Cors.DEFAULT_HEADERS,
},
cloudWatchRole: true,
cloudWatchRoleRemovalPolicy: RemovalPolicy.DESTROY,
deployOptions: {
accessLogDestination: new aws_apigateway.LogGroupLogDestination(
logGroup,
),
accessLogFormat: aws_apigateway.AccessLogFormat.clf(),
},
},
);
restApi.root.addProxy(); // Lambdalithの設定
restApi.root
.addResource('animals')
.addResource('dog')
.addMethod(
'GET',
new aws_apigateway.LambdaIntegration(singlePurposeLambda, {
proxy: true,
}),
); // Single purpose Lambdaの設定
}
}
LambdalithのLambda実装
今回は @codegenie/serverless-express
を利用して実装していきます。
import { injectLambdaContext, Logger } from "@aws-lambda-powertools/logger";
import serverlessExpress from "@codegenie/serverless-express";
import middy from "@middy/core";
import cors from "cors";
import express, { Request, Response } from "express";
const app = express();
app.use(cors());
app.use(express.json());
const logger = new Logger();
app.get(
"/animals/cat",
async (req: Request, res: Response): Promise<void> => {
logger.info(req.path);
const response = {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ animal: "cat" }),
}
res.header(response.headers);
res.status(response.statusCode).send(response.body);
},
);
export const handler = middy(serverlessExpress({ app })).use(
injectLambdaContext(logger),
);
Single Purpose LambdaのLamdba実装
こちらはAPIGatewayでproxy統合をする際の通常の実装です。
GET /animals/dog
のパスで呼び出されるLambda関数です。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
export const handler = async (
event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
console.log(event);
return {
statusCode: 200,
body: JSON.stringify({ animal: 'dog' }),
};
};
動作確認
コンソール上ではこんな感じ。
curlコマンドで確認してみます。
まずは Single Purpose Lambda の実装がうまく呼び出せるか確認します。
curl https://sampleid.execute-api.ap-northeast-1.amazonaws.com/prod/animals/dog
{"animal":"dog"}
続いて、Lambdalithの実装がうまく呼び出せるか確認します。
curl https://sampleid.execute-api.ap-northeast-1.amazonaws.com/prod/animals/cat
{"animal":"cat"}
うまく共存できていますね。
LambdaRestApi
のConstructを使うと共存できない
小ネタ: LambdaRestApi
を利用して addResourceでパスを追加しようとすると、エラーが発生。CDKの実装的に、proxy: trueとなっていると新しいResourceを登録できない。
こちらの実装によるもの。
感想
「最初はLambdalithで実装して必要になったら Single Purpose Lambda を使う」という方法が本当に可能か気になったので検証してみました。
今回の検証で、「とりあえずLambdalith」への抵抗を減らすことができたので、同じことを検討している方の参考になれば幸いです。
(Lambdalithってめっちゃキャッチーだけど、Single purpose Lambda も同じくらい短く表現したい)